#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# this lesson is based on python 3

# ====HIGHER FUNCTION==== 
## MAP/REDUCE
def f(x):
    return x * x

ite = map(f, range(10)) # invoke a function on every item of an Iterable, and return a Iterator
print(list(ite))

from functools import reduce

def product(x, y):
    return x * y

reduce(product, range(1, 5)) 
# apply a function with two parameters on an Iterable. 
# apply last return value with next item, until there is no more item

## FILTER
# filter also invoke a function on every item of an Iterable.
# but according to return True/False, it will keep/delete the item from the final return Iterator

def not_empty(s):
    return s and s.strip()

list(filter(not_empty, ['A', '', 'B', None, 'C', '  ']))
# 结果: ['A', 'B', 'C']

## SORTED return a sorted list
sorted([10, 3, -1, 7]) # return [-1, 3, 7, 10]

# a function can be added as a key. now the list will be sorted after each item is applied by key.
sorted([36, 5, -12, 9, -21], key=abs) # return [5, 9, -12, -21, 36]

# add reverse=True to reverse the default order
sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True)
['Zoo', 'Credit', 'bob', 'about']


# ====RETURN A FUNCTION==== 

# this is a conventional sum calculating function:
def calc_sum(*args): # *args receive multiple undefined number of parameters
    ax = 0
    for n in args:
        ax = ax + n
    return ax

# we can return a function instead of the number sum:
def lazy_sum(*args):
    def sum():
        ax = 0
        for n in args:
            ax = ax + n
        return ax
    return sum

f = lazy_sum(1, 3, 5, 7, 9)
sum = f()
# the inner func sum() can use the parameters and local variables of func lazy_sum()
# when lazy_sum() return sum(), the structure is called 'closure'

f1 = lazy_sum(1, 3, 5, 7, 9)
f != f1

# the returned function only executes when itself is called, not when it is returned
def count():
    fs = []
    for i in range(1, 4): # in range(a,b) b is omitted in the produced sequence
        def f():
             return i*i
        fs.append(f)
    return fs

f1, f2, f3 = count()
f1() == 9
f2() == 9
f3() == 9

# so don't use any looping/changing variables in the returned func of closure
def count2():
    fs = []
    def f(j):
        def g():
            return j*j
        return g
    for i in range(1, 4):
        fs.append(f(i)) # f(i)立刻被执行，因此i的当前值被传入f()
    return fs

f1, f2, f3 = count2()
f1() == 1
f2() == 4
f3() == 9


# ====ANONYMOUS FUNCTION==== 
list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
# use lambda to use an anonymous func instead of define a new named func
f = lambda x: x * x
def func(x):
    return x * x
f = func

# ====DECORATOR==== 
# function is also object, with an attribute '__name__'
func.__name__ # func
f.__name__ # func

def log(func):
    def wrapper(*args, **kw): # key word parameter can receive multiple key-value pair and pack them as a dict
        print('call %s():' % func.__name__)
        return func(*args, **kw) # this parameter comb means 'any parameters'
    return wrapper

@log # put @log before def means 'now = log(now)'
def now():
    print('2015-3-25')

# in this way, log() become a decorator of func now(), so that every time we call now():
now()
# print:
'''
call now():
2015-3-25
'''

def log2(text):
    def decorator(func):
        def wrapper(*args, **kw):
            print('%s %s():' % (text, func.__name__))
            return func(*args, **kw)
        return wrapper
    return decorator

@log('execute') # this means 'now = log('execute')(now)'
def now2():
    print('2015-3-25')

# in this way, log() become a decorator of func now(), so that every time we call now():
now()
# print:
'''
execute now():
2015-3-25
'''
# but wait, now now.__name__ become 'wrapper'!
import functools

def log3(text):
    def decorator(func):
        @functools.wraps(func) # make wrapper.__name__ = func.__name__
        def wrapper(*args, **kw):
            print('%s %s():' % (text, func.__name__))
            return func(*args, **kw)
        return wrapper
    return decorator
# in this way, decorated functions can keep their __name__

# ====PARTIAL FUNCTION====
import functools
# to set default value to some parameters of a func and return a new func
int2 = functools.partial(int, base=2)
# turn buildin func int(), convert a binary number/string to a dec number

print(int2('100')) # print 4

max_with10 = functools.partial(max, 10)

print(max_with10(2, 3, 7)) # print 10